<?php

/**
 * Simple object persistence provider.
 */
abstract class Persistence {
    /**
     * Establishes a connection to the database using the application's
     * configuration.
     */
    public static function connect() {
        dibi::connect(array(
            'driver'   => Config::DB_DRIVER,
            'host'     => Config::DB_SERVER,
            'username' => Config::DB_USER,
            'password' => Config::DB_PASSWORD,
            'database' => Config::DB_NAME,
            'charset'  => 'utf8'
        ));
    }
    
    /**
     * Loads records from the database matching the specified criteria.
     * 
     * @param string $table Name of the database table from which to load the
     *        data.
     * @param array $where Filtering condition. The keys of the array are field
     *        names, the values are required values of the fields.
     * @param array $order Ordering fields. Each field name may be followed by
     *        a whitespace and ASC for ascending order, or DESC for descending
     *        order.
     * @param int $limit The maximum number of returned records.
     * @param int $offset The offset of the first returned record from all
     *        records matching all the other criteria.
     * @return array Loaded records.
     * @throws InvalidArgumentException Thrown if the records could not be
     *         loaded.
     */
    public static function load($table, array $where = array(),
            array $order = array(), $limit = null, $offset = null) {
        // columns, from
        $sql = 'SELECT * FROM %n';
        $params = array();
        $params[] = $table;
        
        // where
        if (!empty($where)) {
            $sql .= ' WHERE %and';
            $params[] = $where;
        }
        
        // order
        if (!empty($order)) {
            $sql .= ' ORDER BY %by';
            $params[] = $order;
        }
        
        // apply limit and offset
        if ($limit) {
            $sql .= ' %lmt';
            $params[] = $limit;
        }
        if ($offset) {
            $sql .= ' %ofs';
            $params[] = $offset;
        }
        
        // add the SQL to the beggining of the arguments
        array_unshift($params, $sql);
        $result = dibi::query($params);
        
        $className = "{$table}Entity";
        if (!class_exists($className)) {
            throw new InvalidArgumentException('The class for table ' .
                    "$table (class $className) is not loaded yet");
        }
        $record = new $className();
        if (!($record instanceof Entity)) {
            throw new InvalidArgumentException('The class ' . $className .
                    ' does not represent a table');
        }
        
        $records = array();
        foreach ($result as $row) {
            $record = new $className();
            $record->load($row->toArray());
            $records[] = $record;
        }
        return $records;
    }
    
    /**
     * Loads a single record identified by the specified primary key.
     * 
     * @param string $table Entity table name.
     * @param mixed $id Primary key field's value.
     * @return Entity The loaded entity or null if the record does not exist.
     * @throws InvalidArgumentException Thrown if the specified table does not
     *         have an entity class defined.
     */
    public static function find($table, $id) {
        $className = "{$table}Entity";
        if (!class_exists($className)) {
            throw new InvalidArgumentException('The class for table ' .
                    "$table (class $className) is not loaded yet");
        }
        $record = new $className();
        if (!($record instanceof Entity)) {
            throw new InvalidArgumentException('The class ' . $className .
                    ' does not represent a table');
        }
        
        $pkField = $record->getPKField();
        $pkType = $record->getPKFieldDibiType();
        $sql = "SELECT * FROM %n WHERE %n = %$pkType %lmt";
        $result = dibi::query($sql, $table, $pkField, $id, 1);
        
        $data = $result->fetch();
        if ($data) {
            $record->load($data->toArray());
            return $record;
        }
        return null;
    }
    
    /**
     * Deletes the provided entity or records matching the specified conditions
     * from the specified table.
     * 
     * @param Entity|string $table Table name or entity to delete.
     * @param array $where Conditions to apply when deleting records. Used only
     *        if $table is string.
     */
    public static function delete($table, array $where = array()) {
        if ($table instanceof Entity) {
            $id = $table->getPKField();
            self::delete($table->getTable(), array($id => $table->$id));
        } else {
            if (empty($where)) {
                dibi::query('DELETE FROM %n', $table);
            } else {
                dibi::query('DELETE FROM %n WHERE %and', $table, $where);
            }
        }
    }
    
    /**
     * Saves the provided entity. The method automatically decided whether the
     * data should be inserted on updated by the presence of the primary key
     * field's value.
     * 
     * @param Entity $entity The entity to save.
     */
    public static function save(Entity $entity) {
        $data = $entity->dump();
        if ($data[$entity->getPKField()]) {
            $id = $data[$entity->getPKField()];
            unset($data[$entity->getPKField()]);
            $type = $entity->getPKFieldDibiType();
            $sql = 'UPDATE %n SET %a WHERE %n = %' . $type;
            dibi::query($sql, $entity->getTable(), $data,
                    $entity->getPKField(), $id);
        } else {
            $sql = 'INSERT INTO %n %v';
            dibi::query($sql, $entity->getTable(), $data);
            $field = $entity->getPKField();
            $entity->$field = dibi::getInsertId();
        }
    }
    
    /**
     * Closes the connection to the database.
     */
    public static function close() {
        dibi::disconnect();
    }
}
